package com.google.code.qsecurity.server;

import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.servlet.http.HttpSession;

import com.google.code.qsecurity.server.domain.EmailVerification;
import com.google.code.qsecurity.server.domain.PasswordRequest;
import com.google.code.qsecurity.server.domain.User;
import com.google.code.qsecurity.server.response.QLoginResponse;
import com.google.code.qsecurity.server.response.QPasswordChangeResponse;
import com.google.code.qsecurity.server.response.QResponse;
import com.google.code.qsecurity.server.response.QSignupResponse;
import com.google.code.qsecurity.server.response.QVerificationResponse;
import com.google.code.qsecurity.server.service.MailService;
import com.google.code.qsecurity.server.util.BCrypt;
import com.google.code.qsecurity.server.util.RequestCodeGenerator;
import com.google.code.qsecurity.server.util.SessionUtil;
import com.google.code.qsecurity.server.util.VerificationCodeGenerator;
import com.google.code.qsecurity.shared.QResponseCode;
import com.google.code.qsecurity.shared.QSettings;
import com.google.code.qsecurity.shared.UserStatus;
import com.google.code.qsecurity.shared.UserType;
import com.google.code.qsecurity.shared.annotation.ServiceMethod;
import com.google.web.bindery.requestfactory.server.RequestFactoryServlet;


public class QService {
	private static final Logger logger = Logger.getLogger(QService.class.getName());

	@ServiceMethod
	public User readUser(String email) {
		logger.log(Level.FINEST, "Attempting to readUser " + email);
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();
		
		User user;
		try {
			user = pm.getObjectById(User.class, email);
		}
		catch (JDOObjectNotFoundException onfe) {
			logger.log(Level.FINEST, "User " + email + " not found", onfe);
			return null;
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to read user", e);
			return null;
		}
		finally {
			pm.close();
		}
		logger.log(Level.INFO, "Successful readUser " + email);
		
		return user;
	}

	@ServiceMethod
	public QResponse updateUser(String oldEmail, User user) {
		logger.log(Level.FINEST, "Attempting to updateUser " + user.getEmail());
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();

		QResponse response = new QResponse();
		User storedUser;
		try {
			storedUser = pm.getObjectById(User.class, oldEmail);
			
			if (!storedUser.getEmail().equals(oldEmail)) {  // TODO do this check on the client side?
				storedUser.setEmail(user.getEmail());
				storedUser.setStatus(UserStatus.PENDING);
				createVerificationCode(user);
			}
			storedUser.setHashedPassword(BCrypt.hashpw(user.getHashedPassword(), BCrypt.gensalt()));
			storedUser.setName(user.getName());
		}
		catch (JDOObjectNotFoundException onfe) {
			logger.log(Level.FINEST, "User " + user.getEmail() + " not found");
			response.setCode(QResponseCode.EMAIL_NOT_FOUND);
			return response;
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to update user", e);
			response.setCode(QResponseCode.SYSTEM_FAILURE);
			return response;
		}
		finally {
			pm.close();  // fields are updated in data store
		}
		logger.log(Level.INFO, "Successful updateUser " + user.getEmail());
		
		return response;
	}

	@ServiceMethod
	public void deleteUser(User user) {
		logger.log(Level.FINEST, "Attempting to deleteUser " + user.getEmail());
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();
		
		try {
			pm.deletePersistent(user);
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to delete user", e);
			// TODO
		}
		finally {
			pm.close();
		}
		logger.log(Level.INFO, "Successful deleteUser " + user.getEmail());
	}

	@ServiceMethod
	public List<User> queryUsers() {
		// TODO Auto-generated method stub
		return null;
	}
	
	@ServiceMethod
	public Boolean isUserLoggedIn(String email) {
		HttpSession session= RequestFactoryServlet.getThreadLocalRequest().getSession();
		return SessionUtil.isUserStored(session, email);
	}
	
	@ServiceMethod
	public QSignupResponse signup(String appName, String adminAddress, User user) {
		logger.log(Level.FINEST, "Attempting to signup " + user.getEmail());
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();
		
		QSignupResponse response = new QSignupResponse();
		User existingUser = null;
		
		// query email address in data store
		try {
			existingUser = pm.getObjectById(User.class, user.getEmail());
		}
		catch (JDOObjectNotFoundException onfe) {
			logger.log(Level.FINEST, "User " + user.getEmail() + " not found. Continuing signup.");
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to lookup user", e);
			response.setCode(QResponseCode.SYSTEM_FAILURE);
			return response;
		}
		finally {
			pm.close();
		}
		
		// check if the email address is already registered
		if (existingUser != null) {
			response.setCode(QResponseCode.EMAIL_ALREADY_REGISTERED);
			logger.log(Level.FINEST, "User " + user.getEmail() + " is already registered. Exiting signup.");
			return response;
		}
		
		// password hasn't been hashed up to this point - hash it before saving user object
		user.setHashedPassword(BCrypt.hashpw(user.getHashedPassword(), BCrypt.gensalt()));
		
		// initialize user's type and status
		user.setType(UserType.STANDARD);
		user.setStatus(UserStatus.PENDING);
		
		if (createUser(user) == null) {
			response.setCode(QResponseCode.SYSTEM_FAILURE);
			return response;
		}
		
		response.setEmail(user.getEmail());
		
		String verificationCode = createVerificationCode(user);
		logger.log(Level.FINEST, "Verification password " + verificationCode + " created for " + user.getEmail());
		
		// Send email to user with verification code
		if (MailService.getInstance().send(adminAddress, appName,
				user.getEmail(), user.getName(),
				//TODO message body & subject internationalization
				"Your verification code is " + verificationCode, appName + " Verification")) {
			logger.log(Level.FINEST, "Verification mail successfully sent to " + user.getEmail());
		}
		else {
			logger.log(Level.WARNING, "Verification mail could not be sent to " + user.getEmail());
			// TODO put in queue
		}
		
		response.setCode(QResponseCode.SUCCESS);
		
		logger.log(Level.INFO, "Successful signup " + user.getEmail());
		
		return response;
	}
	
	@ServiceMethod
	public QLoginResponse login(User user) {
		logger.log(Level.FINEST, "Attempting to login " + user.getEmail());
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();

		QLoginResponse response = new QLoginResponse();
		User storedUser, detachedUser = null;
		try {
			storedUser = pm.getObjectById(User.class, user.getEmail());
			detachedUser = pm.detachCopy(storedUser);
		}
		catch (JDOObjectNotFoundException onfe) {
			logger.log(Level.FINEST, "Rejecting login due to unregistered email " + user.getEmail());
			response.setCode(QResponseCode.EMAIL_NOT_FOUND);
			return response;
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to lookup user", e);
			response.setCode(QResponseCode.SYSTEM_FAILURE);
			return response;
		}
		finally {
			pm.close();
		}
		
		if (storedUser.getStatus() == UserStatus.PENDING) {
			logger.log(Level.FINEST, "Rejecting login due to unverified email " + user.getEmail());
			response.setCode(QResponseCode.EMAIL_NOT_VERIFIED);
			return response;
		}
		
		if (BCrypt.checkpw(user.getHashedPassword(), storedUser.getHashedPassword())) {
			// update user's last login date
			detachedUser.setLastLogin(new Date());
			logger.log(Level.FINEST, "Last login date set to " + user.getLastLogin());
			
			// attach user back to the persistence graph
			pm = QPersistenceManagerFactory.get().getPersistenceManager();
			try {
				pm.makePersistent(detachedUser);
			}
			catch (Exception e) {
				logger.log(Level.SEVERE, "Unable to update user ", e);
				response.setCode(QResponseCode.SYSTEM_FAILURE);
				return response;
			}
			finally {
				pm.close();
			}
			
			HttpSession session = RequestFactoryServlet.getThreadLocalRequest().getSession();
			
			// store email in server's session object
			Date sessionExpiration = SessionUtil.initSession(session, storedUser.getEmail());
			
			// update response
			response.setSessionId(session.getId());
			response.setSessionExpiration(sessionExpiration);
			response.setUser(storedUser);
			response.setCode(QResponseCode.SUCCESS);
		}
		else {
			logger.log(Level.FINEST, "Rejecting login due to invalid password " + user.getEmail());
			response.setCode(QResponseCode.INVALID_PASSWORD);
		}
		
		logger.log(Level.FINEST, "Successful login " + user.getEmail());
			
		return response;
	}
	
	@ServiceMethod
	public void logout(String email) {
		HttpSession session = RequestFactoryServlet.getThreadLocalRequest().getSession();
		SessionUtil.removeUser(session, email);
	}
	
	@ServiceMethod
	public QVerificationResponse verifySignupEmail(EmailVerification verification) {
		logger.log(Level.FINEST, "Attempting to verify " + verification.getEmail());
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();

		QVerificationResponse response = new QVerificationResponse();
		EmailVerification storedVerification, detachedVerification = null;
		// get stored validation object
		try {
			storedVerification = pm.getObjectById(EmailVerification.class, verification.getEmail());
			detachedVerification = pm.detachCopy(storedVerification);
		}
		catch (JDOObjectNotFoundException onfe) {
			logger.log(Level.FINEST, "Rejecting verification due to unregistered email " + verification.getEmail());
			response.setCode(QResponseCode.EMAIL_NOT_FOUND);
			return response;
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to lookup user", e);
			response.setCode(QResponseCode.SYSTEM_FAILURE);
			return response;
		}
		
		if (storedVerification.getCode() != null && storedVerification.getCode().equals(verification.getCode())) {
			pm = QPersistenceManagerFactory.get().getPersistenceManager();
			User user;
			try {
				user = pm.getObjectById(User.class, storedVerification.getEmail());
				
				user.setStatus(UserStatus.VERIFIED);
				
				user.setLastLogin(new Date());
			}
			catch (Exception e) {
				logger.log(Level.SEVERE, "Error looking up user with correct verification", e);
				response.setCode(QResponseCode.SYSTEM_FAILURE);
				return response;
			}
			finally {
				pm.close();  // user is updated
			}
			logger.log(Level.FINEST, "Status set to " + user.getStatus());
			logger.log(Level.FINEST, "Last login date set to " + user.getLastLogin());
			
			HttpSession session = RequestFactoryServlet.getThreadLocalRequest().getSession();

			// store email in server's session object
			response.setSessionExpiration(SessionUtil.initSession(session, verification.getEmail()));
			
			// update response
			response.setSessionId(session.getId());
			response.setUser(user);
			
			// delete validation object
			pm = QPersistenceManagerFactory.get().getPersistenceManager();
			try {
				pm.deletePersistent(detachedVerification);
			}
			catch (Exception e) {
				logger.log(Level.SEVERE, "Unable to delete verification entry ", e);
				response.setCode(QResponseCode.SYSTEM_FAILURE);
				return response;
			}
			finally {
				pm.close();
			}
		}
		else {
			logger.log(Level.FINEST, "Rejecting verification due to invalid code " + verification.getEmail());
			response.setCode(QResponseCode.INVALID_VERIFICATION_CODE);
			return response;
		}
		
		logger.log(Level.FINEST, "Successful verification " + verification.getEmail());
		
		response.setCode(QResponseCode.SUCCESS);

		return response;
	}
	
	@ServiceMethod
	public QPasswordChangeResponse processPasswordRequest(String adminAddress, String appName, String email) {
		logger.log(Level.FINEST, "Password request received from " + email);
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();

		// Query user
		QPasswordChangeResponse response = new QPasswordChangeResponse();
		User storedUser, detachedUser = null;
		try {
			storedUser = pm.getObjectById(User.class, email);
			detachedUser = pm.detachCopy(storedUser);
		}
		catch (JDOObjectNotFoundException onfe) {
			logger.log(Level.FINEST, "Rejecting login due to unregistered email " + email);
			response.setCode(QResponseCode.EMAIL_NOT_FOUND);
			return response;
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to lookup user", e);
			response.setCode(QResponseCode.SYSTEM_FAILURE);
			return response;
		}
		finally {
			pm.close();
		}
		
		// Get last request date and time
		Date lastPasswordRequest = detachedUser.getLastPasswordRequest();
		
		// calculate time 24 hours ago
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.HOUR_OF_DAY, - QSettings.PASSWORD_REQUEST_INTERVAL_HOURS);
		
		// Check that last request was made more than 24 hours ago
		if (lastPasswordRequest.before(calendar.getTime())) {  // more than 24hrs ago
			// Update last request date to now
			detachedUser.setLastPasswordRequest(new Date());
			
			String verificationCode = createPasswordRequestCode(detachedUser);
			logger.log(Level.FINEST, "Verification password " + verificationCode + " created for " + detachedUser.getEmail());
			
			// Create link with request code and email it to user
			if (MailService.getInstance().send(adminAddress, appName,
					detachedUser.getEmail(), detachedUser.getName(),
					//TODO message body & subject internationalization
					//TODO produce reset code
					//TODO send a reset link instead of hashed password
					"Your password is " + detachedUser.getHashedPassword(), appName + " Verification")) {
				logger.log(Level.FINEST, "Verification mail successfully sent to " + detachedUser.getEmail());
			}
			else {
				logger.log(Level.WARNING, "Verification mail could not be sent to " + detachedUser.getEmail());
				// TODO put in queue
			}
		}
		else {
			// Calculate time remaining for next request
			long msRemaining = QSettings.PASSWORD_REQUEST_INTERVAL_MILLISECONDS - calendar.getTimeInMillis();
			
			// TODO send error with time remaining until next request is allowed
		}
		
		// TODO if no request within 24 hours email password else send time remaining
		
		logger.log(Level.FINEST, "Password request successfully processed for " + email);
		
		response.setCode(QResponseCode.SUCCESS);

		return response;
	}
	
	@ServiceMethod
	public QPasswordChangeResponse verifyPasswordChangeLink(PasswordRequest request) {
		logger.log(Level.FINEST, "Attempting to verify link " + request.getCode());
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();

		QPasswordChangeResponse response = new QPasswordChangeResponse();
		EmailVerification storedVerification, detachedVerification = null;
		// get stored validation object
		try {
			storedVerification = pm.getObjectById(EmailVerification.class, request.getEmail());
			detachedVerification = pm.detachCopy(storedVerification);
		}
		catch (JDOObjectNotFoundException onfe) {
			logger.log(Level.FINEST, "Rejecting password request due to unregistered email " + request.getEmail());
			response.setCode(QResponseCode.EMAIL_NOT_FOUND);
			return response;
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to lookup user", e);
			response.setCode(QResponseCode.SYSTEM_FAILURE);
			return response;
		}
		
		if (storedVerification.getCode() != null && storedVerification.getCode().equals(request.getCode())) {
			pm = QPersistenceManagerFactory.get().getPersistenceManager();
			User user;
			try {
				user = pm.getObjectById(User.class, storedVerification.getEmail());
				
				user.setLastPasswordRequest(new Date());
			}
			catch (Exception e) {
				logger.log(Level.SEVERE, "Error looking up user with correct verification", e);
				response.setCode(QResponseCode.SYSTEM_FAILURE);
				return response;
			}
			finally {
				pm.close();  // user is updated
			}
			logger.log(Level.FINEST, "Status set to " + user.getStatus());
			logger.log(Level.FINEST, "Last password request date set to " + user.getLastLogin());

			// store email in server's session object
			response.setEmail(request.getEmail());
			
			// delete validation object
			pm = QPersistenceManagerFactory.get().getPersistenceManager();
			try {
				pm.deletePersistent(detachedVerification);
			}
			catch (Exception e) {
				logger.log(Level.SEVERE, "Unable to delete verification entry ", e);
				response.setCode(QResponseCode.SYSTEM_FAILURE);
				return response;
			}
			finally {
				pm.close();
			}
		}
		else {
			logger.log(Level.FINEST, "Rejecting request due to invalid code " + request.getEmail());
			response.setCode(QResponseCode.INVALID_VERIFICATION_CODE);
			return response;
		}
		
		logger.log(Level.FINEST, "Successful verification " + request.getEmail());
		
		response.setCode(QResponseCode.SUCCESS);

		return response;
	}
	
	// Helper methods
	
	private User createUser(User user) {
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();
		
		try {
			user = pm.makePersistent(user);
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to create user", e);
			return null;
		}
		
		return user;
	}

	private String createVerificationCode(User user) {
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();
		
		EmailVerification validation = new EmailVerification();
		validation.setCode(VerificationCodeGenerator.generate());
		validation.setDate(new Date());  // set date to now
		validation.setEmail(user.getEmail());
		try {
			validation = pm.makePersistent(validation);
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to save verification", e);
			return null;
		}
		
		return validation.getCode();
	}

	private String createPasswordRequestCode(User user) {
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();
		
		PasswordRequest request = new PasswordRequest();
		request.setCode(RequestCodeGenerator.generate());
		request.setDate(new Date());  // set date to now
		request.setEmail(user.getEmail());
		try {
			request = pm.makePersistent(request);
		}
		catch (Exception e) {
			logger.log(Level.SEVERE, "Unable to save password request", e);
			return null;
		}
		
		return request.getCode();
	}

	public static EmailVerification createEmailVerification() {
		// TODO Auto-generated method stub
		return null;
	}
	
	public static EmailVerification readEmailVerification(Long id) {
		// TODO Auto-generated method stub
		return null;
	}
	
	public static EmailVerification updateEmailVerification(
			EmailVerification emailverification) {
		// TODO Auto-generated method stub
		return null;
	}
	
	public static void deleteEmailVerification(EmailVerification emailverification) {
		PersistenceManager pm = QPersistenceManagerFactory.get().getPersistenceManager();
		try {
			pm.deletePersistent(emailverification);
		}
		catch (Exception e) {
			// TODO
			e.printStackTrace();
		}
		finally {
			pm.close();
		}
	}
	
	public static List<EmailVerification> queryEmailVerifications() {
		// TODO Auto-generated method stub
		return null;
	}

}
